/*
 * Copyright (C) 2001-2006 the xine project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 * $Id: gtkvideo.c,v 1.115 2006/04/04 22:08:16 dsalt Exp $
 *
 * gtk xine video widget
 *
 * some code originating from totem's gtkxine widget
 *
 * dbus/gnome-screensaver interaction code originating from totem's
 * screensaver object.
 * Original copyright for this code, according to the commit log:
 *   Copyright (C) 2005-2006 William Jon McCann <mccann@jhu.edu>
 */

#include "config.h"
#include "i18n.h"

#include "defs.h"

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <pwd.h>
#include <sys/types.h>
#include <pthread.h>
#include <sched.h>

#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/cursorfont.h>
#include <X11/Xatom.h>
#ifdef HAVE_DPMS_EXTENSION
#include <X11/extensions/dpms.h>
#endif
#ifdef HAVE_XTESTEXTENSION
#include <X11/extensions/XTest.h>
#endif
#ifdef HAVE_XINERAMA
#include <X11/extensions/Xinerama.h>
#endif

#ifdef WITH_DBUS
#include <dbus/dbus-glib.h>

#define GS_LEGACY_SERVICE	"org.gnome.screensaver"
#define GS_LEGACY_PATH		"/org/gnome/screensaver"
#define GS_LEGACY_INTERFACE	"org.gnome.screensaver"

#define GS_SERVICE		"org.gnome.ScreenSaver"
#define GS_PATH			"/org/gnome/ScreenSaver"
#define GS_INTERFACE		"org.gnome.ScreenSaver"
#endif

#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <gtk/gtkwidget.h>
#include <gtk/gtkmain.h>
#include <gdk/gdkx.h>
#include <xine.h>

#include "gtkvideo.h"
#include "globals.h"
#include "post.h"
#include "playlist.h"
#include "utils.h"

/* missing stuff from X includes */
#ifndef XShmGetEventBase
extern int XShmGetEventBase(Display *);
#endif

static void gtk_video_class_init    (GtkVideoClass   *klass);
static void gtk_video_instance_init (GtkVideo        *gvideo);

static void gtk_video_finalize      (GObject        *object);
static void gtk_video_realize       (GtkWidget      *widget);
static void gtk_video_unrealize     (GtkWidget      *widget);
static void gtk_video_map (GtkWidget *);

/*
static void gtk_video_size_request (GtkWidget      *widget,
		       		    GtkRequisition *requisition);
*/
static void gtk_video_size_allocate (GtkWidget      *widget,
 				     GtkAllocation  *allocation);

static gboolean gtv_unblank_screen (GtkVideo *);

static GtkWidgetClass *parent_class = NULL;
static pthread_mutex_t resize_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t resize_add_lock = PTHREAD_MUTEX_INITIALIZER;

#define RESIZE_LOCK() pthread_mutex_lock (&resize_lock)
#define RESIZE_UNLOCK() pthread_mutex_unlock (&resize_lock)

#define RESIZE_ADD() \
  do { \
    pthread_mutex_lock (&resize_add_lock); \
    if (!gtv->priv->resizing && GTK_WIDGET_REALIZED (&gtv->widget)) \
      gtv->priv->resizing = \
	g_idle_add_full (G_PRIORITY_HIGH_IDLE, \
			 (GSourceFunc) gtk_video_idle_resize, gtv, NULL); \
    pthread_mutex_unlock (&resize_add_lock); \
  } while (0)

/*
 * private data
 */

struct gtk_video_private_s {

  xine_t                  *xine;
  xine_stream_t           *stream;
  xine_post_out_t         *out;
  char                    *video_driver_id;
  xine_video_port_t       *video_port;

  Window                   video_window;
  pthread_t                thread;

  double                   display_ratio;

  /* On exit from full-screen mode, we get multiple allocate events.
   * Consequently, there may be multiple rescalings.
   * We *don't* necessarily want to signal them.
   */
  gboolean		   block_next_scale_change:8;

  gboolean                 auto_resize:8, auto_rescale:8, pending_resize:8;
  GtkRequisition	   pos, size, old_size, video_size, old_video_size;
  float                    resize_factor;
  guint			   resizing, minimising;

  int			   button_press_mask; /* pass to front end? */
  int			   button_release_mask; /* pass to front end? */
  int			   button_event_shifted; /* was shifted? */

  gboolean		   idle_resized:8;
  gboolean		   hinted:8;

  /* fullscreen stuff */

  gboolean		   fullscreen_mode:8, pointer_visible:8;
  GtkRequisition	   fullscreen;
  Cursor                   pointer[2];
  guint			   pointer_hide_timeout, screen_blanker_timeout;
  guint			   unblock_timeout;

  gint			   screen_signal;

#ifdef HAVE_XTESTEXTENSION
  Bool                   have_xtest;
  KeyCode                kc_shift[2]; /* fake keypresses */
#endif

#ifdef WITH_DBUS
  DBusGConnection	*connection;
  DBusGProxy		*gs_proxy;
#endif

  gboolean		 playing_no_blank:8;
  /* visualization */
  gboolean		   vis_active:8;
  xine_post_t		  *vis_plugin;
  char			  *vis_plugin_id;
  xine_audio_port_t       *vis_audio;

  /* post-plugins */
  struct {
    GList	*list;
    gboolean	 enable;
  }			   post_deinterlace, post_video, post_audio;
  gboolean		   have_tvtime;
};

static int gtv_table_signals[LAST_SIGNAL] = { 0 };

#ifdef HAVE_XTESTEXTENSION
static int shift = 0; /* Shift key state */
#endif

static inline GdkDisplay *gtv_get_display (GtkVideo *gtv)
{
  return gdk_drawable_get_display ((GdkDrawable *)gtv->widget.window);
}

static inline Display *gtv_get_xdisplay (GtkVideo *gtv)
{
  return GDK_WINDOW_XDISPLAY (gtv->widget.window);
}

static inline GdkScreen *gtv_get_screen (GtkVideo *gtv)
{
  return gdk_drawable_get_screen ((GdkDrawable *)gtv->widget.window);
}

static inline Screen *gtv_get_xscreen (GtkVideo *gtv)
{
  return GDK_SCREEN_XSCREEN (gtv_get_screen (gtv));
}

static inline int gtv_get_xscreen_num (GtkVideo *gtv)
{
  return GDK_SCREEN_XNUMBER (gtv_get_screen (gtv));
}

static inline gboolean is_fullscreen (gtk_video_private_t *priv)
{
  return priv->fullscreen_mode == 1;
}

#ifdef WITH_DBUS
static inline gboolean
screensaver_is_running_dbus (gtk_video_private_t *priv)
{
  return priv->connection && priv->gs_proxy;
}

static void
gtv_screensaver_inhibit_dbus (gtk_video_private_t *priv, gboolean inhibit)
{
  GError *error = NULL;
  gboolean res;

  if (!screensaver_is_running_dbus (priv))
    return;

  if (inhibit)
  {
    char *reason;
    reason = g_strdup (_("gxine is active"));
    res = dbus_g_proxy_call (priv->gs_proxy, "InhibitActivation", &error,
			     G_TYPE_STRING, reason,
			     G_TYPE_INVALID, G_TYPE_INVALID);
    g_free (reason);
  }
  else
    res = dbus_g_proxy_call (priv->gs_proxy, "AllowActivation", &error,
			     G_TYPE_INVALID, G_TYPE_INVALID, G_TYPE_INVALID);

  if (!res && error)
    g_printerr (
	     _("gtkvideo: problem when inhibiting the GNOME screensaver: %s\n"),
	     error->message);
  if (error)
    g_error_free (error);
}

static inline void
gtv_screensaver_maybe_inhibit_dbus (gtk_video_private_t *priv, gboolean inhibit)
{
  if (screensaver_is_running_dbus (priv))
    gtv_screensaver_inhibit_dbus (priv, inhibit);
}

static void
gtv_gs_proxy_destroy_cb (GObject *obj, gtk_video_private_t *priv)
{
  g_printerr (_("gtkvideo: the GNOME screensaver has left the bus\n"));
  priv->gs_proxy = NULL; /* invalidate only */
}

static void
gtv_init_dbus (GtkVideo *gtv)
{
  GError *error = NULL;
  gtk_video_private_t *priv = gtv->priv;
  priv->connection = dbus_g_bus_get (DBUS_BUS_SESSION, &error);

  if (!priv->connection)
  {
    if (error)
    {
      g_printerr (
	       _("gtkvideo: failed to connect to the dbus session bus: %s\n"),
	       error->message);
      g_error_free (error);
    }
    return;
  }

  priv->gs_proxy =
    dbus_g_proxy_new_for_name_owner (priv->connection, GS_SERVICE, GS_PATH,
				     GS_INTERFACE, NULL)
    ? : dbus_g_proxy_new_for_name_owner (priv->connection,
					 GS_LEGACY_SERVICE, GS_LEGACY_PATH,
					 GS_LEGACY_INTERFACE, NULL);
  if (priv->gs_proxy)
    g_signal_connect_object (priv->gs_proxy, "destroy",
			     G_CALLBACK (gtv_gs_proxy_destroy_cb), gtv, 0);
  else
    g_printerr (
	     _("gtkvideo: failed to get a proxy for gnome-screensaver\n"));
}

static void
gtv_finalise_dbus (gtk_video_private_t *priv)
{
  if (priv->gs_proxy)
  {
    g_object_unref (priv->gs_proxy);
    priv->gs_proxy = NULL;
  }
}
#else
#define screensaver_is_running_dbus(PRIV) (FALSE)
#define gtv_screensaver_maybe_inhibit_dbus(PRIV, INHIBIT)
#define gtv_init_dbus(PRIV)
#define gtv_finalise_dbus(PRIV)
#endif

GtkType gtk_video_get_type (void)
{
  static GtkType gtk_video_type = 0;

  if (!gtk_video_type)
  {
    static const GTypeInfo gtk_video_info = {
      sizeof (GtkVideoClass),
      (GBaseInitFunc) NULL,
      (GBaseFinalizeFunc) NULL,
      (GClassInitFunc) gtk_video_class_init,
      (GClassFinalizeFunc) NULL,
      NULL /* class_data */,
      sizeof (GtkVideo),
      0 /* n_preallocs */,
      (GInstanceInitFunc) gtk_video_instance_init,
    };

    gtk_video_type = g_type_register_static (GTK_TYPE_WIDGET,
					     "GtkVideo", &gtk_video_info, (GTypeFlags)0);
  }

  return gtk_video_type;
}

static void gtv_set_pointer (GtkVideo *gtv, gboolean state)
{
  Display *display = gtv_get_xdisplay (gtv);
  gtv->priv->pointer_visible = state;
  XLockDisplay (display);
  XDefineCursor (display, gtv->priv->video_window, gtv->priv->pointer[!!state]);
  XFlush (display);
  XUnlockDisplay (display);
}

static gboolean gtv_hide_pointer_cb (GtkVideo *gtv)
{
  gtv->priv->pointer_hide_timeout = 0;
  gtv_set_pointer (gtv, FALSE);
  return FALSE;
}

static void gtv_show_pointer (GtkVideo *gtv)
{
  if (gtv->priv->pointer_hide_timeout)
  {
    g_source_remove (gtv->priv->pointer_hide_timeout);
    gtv->priv->pointer_hide_timeout = 0;
  }
  gtv_set_pointer (gtv, TRUE);
  if (is_fullscreen (gtv->priv))
    gtv->priv->pointer_hide_timeout =
      g_timeout_add (4000, (GSourceFunc)gtv_hide_pointer_cb, gtv);
}

static void gtv_hide_pointer (GtkVideo *gtv)
{
  if (gtv->priv->pointer_hide_timeout)
  {
    g_source_remove (gtv->priv->pointer_hide_timeout);
    gtv->priv->pointer_hide_timeout = 0;
  }
  gtv_set_pointer (gtv, FALSE);
}

#ifdef HAVE_XTESTEXTENSION
/* Track the Shift keys: we need to ensure that they "remain pressed" when
 * blocking the screen blanker when in full-screen mode.
 */
static guint key_snoop = 0; /* handler ID */
static gboolean key_snoop_cb (GtkWidget *w, GdkEventKey *e, gpointer d)
{
  if (e->type == GDK_KEY_PRESS)
    switch (e->keyval)
    {
    case GDK_Shift_L: shift |= 1; break;
    case GDK_Shift_R: shift |= 2; break;
    }
  else
    switch (e->keyval)
    {
    case GDK_Shift_L: shift &= ~1; break;
    case GDK_Shift_R: shift &= ~2; break;
    }
  return FALSE;
}

#endif

static void gtk_video_class_init (GtkVideoClass *class)
{
  GObjectClass    *object_class;
  GtkWidgetClass  *widget_class;

  object_class = (GObjectClass *) class;
  widget_class = (GtkWidgetClass *) class;

  parent_class = gtk_type_class (gtk_widget_get_type ());

  /* GtkWidget */
  widget_class->realize       = gtk_video_realize;
  widget_class->map	      = gtk_video_map;
  widget_class->unrealize     = gtk_video_unrealize;
  widget_class->size_allocate = gtk_video_size_allocate;
  /*widget_class->size_request  = gtk_video_size_request;*/
  widget_class->expose_event  = (typeof (widget_class->expose_event)) gtk_true;

  /* GObject */
  object_class->set_property  = NULL;
  object_class->get_property  = NULL;
  object_class->finalize      = gtk_video_finalize;

  gtv_table_signals[GTK_VIDEO_SCALE_FACTOR] =
    g_signal_new ("scale-factor-changed",
	G_TYPE_FROM_CLASS (object_class),
	G_SIGNAL_RUN_LAST,
	G_STRUCT_OFFSET (GtkVideoClass, scale_factor),
	NULL, NULL,
	g_cclosure_marshal_VOID__DOUBLE,
	G_TYPE_NONE, 1, G_TYPE_DOUBLE);

#ifdef HAVE_XTESTEXTENSION
  if (!key_snoop)
    key_snoop = gtk_key_snooper_install (key_snoop_cb, NULL);
#endif
}

static void gtk_video_instance_init (GtkVideo *this)
{
  this->widget.requisition.width  = GTK_VIDEO_MIN_WIDTH;
  this->widget.requisition.height = GTK_VIDEO_MIN_HEIGHT;
}

static void gtk_video_finalize (GObject *object)
{
  GtkVideo *gtv = (GtkVideo *) object;
  gtv_finalise_dbus (gtv->priv);
  G_OBJECT_CLASS (parent_class)->finalize (object);
  gtv = NULL;
}

static void gtv_do_resize (GtkVideo *gtv, const GtkRequisition *size)
{
  gtk_widget_set_size_request (&gtv->widget, size->width, size->height);
  if (GTK_WIDGET_REALIZED(&gtv->widget) && gtv->widget.parent)
  {
    gtk_window_resize ((GtkWindow *)gtk_widget_get_toplevel (&gtv->widget),
		       GTK_VIDEO_MIN_WIDTH, 1); /* note controlling geometry */
    gtk_video_allow_shrink (gtv);
  }
}

static void gtv_do_rescale (GtkVideo *gtv)
{
  gtv_do_resize (gtv, &gtv->priv->size);
  gtv->priv->old_size = gtv->priv->size;
  gtv->priv->pending_resize = FALSE;
}

#define SCALE(factor) \
  (GtkRequisition) { \
		     round (priv->video_size.width * (factor) / 100), \
		     round (priv->video_size.height * (factor) / 100) \
		   }

#define IS_DOUBLE(W,H) (priv->auto_rescale \
			&& (W) <= priv->fullscreen.width / 3.0 \
			&& (H) <= priv->fullscreen.height / 3.0)
#define IS_DOUBLE_STD() \
  (IS_DOUBLE(priv->video_size.width, priv->video_size.height))

#define DOUBLE(RF,W,H) ((RF) * (1 + IS_DOUBLE((W),(H))))
#define DOUBLE_STD() \
  (DOUBLE(priv->resize_factor, priv->video_size.width, priv->video_size.height))

static void dest_size_cb (void *gv_gen,
			  int video_width, int video_height,
			  double video_pixel_aspect,
			  int *dest_width, int *dest_height,
			  double *dest_pixel_aspect)
{
  GtkVideo            *gv = (GtkVideo *) gv_gen;
  gtk_video_private_t *priv = gv->priv;

  if (is_fullscreen (priv))
  {
    *dest_width  = priv->fullscreen.width;
    *dest_height = priv->fullscreen.height;
  }
  else
  {
    *dest_width  = gv->widget.allocation.width;
    *dest_height = gv->widget.allocation.height;
  }

  *dest_pixel_aspect = priv->display_ratio;
}

static gboolean gtk_video_idle_resize (GtkVideo *gtv)
{
  gdk_threads_enter ();
  RESIZE_LOCK ();

  gtk_video_private_t *priv = gtv->priv;
  double factor = DOUBLE_STD();
  priv->size = SCALE (factor);

  logprintf ("gtkvideo: idle resize to %d x %d (factor %lf)\n",
             priv->size.width, priv->size.height, factor);

  if (is_fullscreen (priv) || !GTK_WIDGET_VISIBLE (&gtv->widget))
  {
    priv->pending_resize = TRUE;
    logprintf ("gtkvideo: idle deferred (full-screen mode or widget hidden)\n");
  }
  else if (!priv->pending_resize)
  {
    gtv_do_resize (gtv, &priv->size);
    logprintf ("gtkvideo: idle signal done\n");
  }
  else
  {
    gtv_do_rescale (gtv);
    logprintf ("gtkvideo: idle signal done, pending resize handled\n");
  }

  priv->resizing = 0;
  priv->idle_resized = TRUE;

  RESIZE_UNLOCK ();
  gdk_threads_leave ();

  return FALSE;
}

static void frame_output_cb (void *gv_gen,
			     int video_width, int video_height,
			     double video_pixel_aspect,
			     int *dest_x, int *dest_y,
			     int *dest_width, int *dest_height,
			     double *dest_pixel_aspect,
			     int *win_x, int *win_y)
{
  GtkVideo            *gtv = (GtkVideo *) gv_gen;
  gtk_video_private_t *priv;
  static gboolean      first = TRUE; /* handle hw which can't scale video */

  if (gtv == NULL)
    return;

  priv = gtv->priv;

  /* correct size with video_pixel_aspect */
  if (video_pixel_aspect >= priv->display_ratio)
    video_width  = video_width * video_pixel_aspect / priv->display_ratio + .5;
  else
    video_height = video_height * priv->display_ratio / video_pixel_aspect + .5;

  priv->video_size = (GtkRequisition){ video_width, video_height };
  *dest_y = *dest_x = 0;

  if (first || priv->auto_resize)
  {
    RESIZE_LOCK ();
    {
      double factor = DOUBLE(priv->resize_factor, video_width, video_height);
      video_width = round (video_width * factor / 100);
      video_height = round (video_height * factor / 100);

      /* size changed? */
      if (first ||
	  video_width != priv->size.width || video_height != priv->size.height)
      {
        if (is_fullscreen (priv) || !GTK_WIDGET_VISIBLE (&gtv->widget))
        {
          priv->size = (GtkRequisition){ video_width, video_height };
          priv->pending_resize = TRUE;
        }
        else
	  RESIZE_ADD ();
      }
    }
    RESIZE_UNLOCK ();
    first = FALSE;
  }

  if (is_fullscreen (priv))
  {
    *win_x = 0;
    *win_y = 0;
    *dest_width  = priv->fullscreen.width;
    *dest_height = priv->fullscreen.height;
  }
  else
  {
    *win_x = priv->pos.width;
    *win_y = priv->pos.height;
    *dest_width  = gtv->widget.allocation.width;
    *dest_height = gtv->widget.allocation.height;
  }

  *dest_pixel_aspect = priv->display_ratio;
}

static xine_video_port_t *load_video_out_driver (GtkVideo *this)
{
  double                   res_h, res_v;
  x11_visual_t             vis;
  const char              *video_driver_id;
  xine_video_port_t       *video_port;
  gtk_video_private_t     *priv = this->priv;

  vis.display           = gtv_get_xdisplay (this);
  vis.screen            = gtv_get_xscreen_num (this);
  vis.d                 = priv->video_window;
  res_h                 = (DisplayWidth  (vis.display, vis.screen)*1000
			   / DisplayWidthMM (vis.display, vis.screen));
  res_v                 = (DisplayHeight (vis.display, vis.screen)*1000
			   / DisplayHeightMM (vis.display, vis.screen));
  priv->display_ratio   = res_v / res_h;

  if (fabs(priv->display_ratio - 1.0) < 0.01)
    priv->display_ratio   = 1.0;

  vis.dest_size_cb      = dest_size_cb;
  vis.frame_output_cb   = frame_output_cb;
  vis.user_data         = this;

  if (priv->video_driver_id)
    video_driver_id = priv->video_driver_id;
  else
  {
    char **driver_ids = (char **) xine_list_video_output_plugins (priv->xine);
    char **choices = malloc (sizeof (char *) * 100);
    choices[0] = "auto";

    int i = -1;
    while (driver_ids[++i])
      choices[i + 1] = strdup(driver_ids[i]);
    choices[i + 1]=0;

    /* try to init video with stored information */
    i = xine_config_register_enum (priv->xine,
				   "video.driver", 0,
				   choices,
				   N_("video driver to use"),
				   NULL, 10, NULL, NULL);
    video_driver_id = choices[i];
  }
  if (strcmp (video_driver_id, "auto"))
  {
    video_port=xine_open_video_driver (priv->xine,
				      video_driver_id,
				      XINE_VISUAL_TYPE_X11,
				      (void *) &vis);
    if (video_port)
      return video_port;
    else
      g_printerr (_("gtkvideo: video driver %s failed.\n"),
	       video_driver_id); /* => auto-detect */
  }

  return xine_open_video_driver (priv->xine, NULL,
				 XINE_VISUAL_TYPE_X11,
				 (void *) &vis);
}

static void gtv_send_xine_mouse_event (gtk_video_private_t *priv,
				       int x, int y, int button)
{
  x11_rectangle_t   rect;
  xine_event_t      xev;
  xine_input_data_t input;

  rect.x = x;
  rect.y = y;
  rect.w = 0;
  rect.h = 0;

  xine_port_send_gui_data (priv->video_port,
                           XINE_GUI_SEND_TRANSLATE_GUI_TO_VIDEO,
                           (void *) &rect);

  xev.type	  = button ? XINE_EVENT_INPUT_MOUSE_BUTTON
			   : XINE_EVENT_INPUT_MOUSE_MOVE;
  xev.data	  = &input;
  xev.data_length = sizeof (input);
  input.button    = button;
  input.x	  = rect.x;
  input.y	  = rect.y;

  xine_event_send (priv->stream, &xev);
}

static GdkFilterReturn xevent_filter_cb (XEvent *const xev, GdkEvent *const ev,
					 GtkVideo *const gtv)
{
  gtk_video_private_t *const priv = ((GtkVideo *) gtv)->priv;

  if (xev->xany.window != GDK_WINDOW_XWINDOW (gtv->widget.window))
    return GDK_FILTER_CONTINUE;

  switch (xev->type)
  {
  case Expose:
    if (!xev->xexpose.count)
      xine_port_send_gui_data (priv->video_port,
			       XINE_GUI_SEND_EXPOSE_EVENT, xev);
    logprintf ("gtkvideo: expose event: %d,%d %dx%d %d\n",
	       xev->xexpose.x, xev->xexpose.y,
	       xev->xexpose.width, xev->xexpose.height, xev->xexpose.count);
    return GDK_FILTER_REMOVE;

  case FocusIn:
    return GDK_FILTER_REMOVE; /* otherwise the window might be blanked :-| */

  case FocusOut:
    return GDK_FILTER_REMOVE;

  case MotionNotify:
    logprintf ("gtkvideo: mouse event: mx=%d my=%d\n",
	       xev->xmotion.x, xev->xmotion.y);
    gtv_send_xine_mouse_event (priv, xev->xmotion.x, xev->xmotion.y, 0);
    if (priv->fullscreen_mode)
      gtv_show_pointer (gtv);
    return GDK_FILTER_CONTINUE;

  case ButtonPress:
    logprintf ("gtkvideo: mouse button press: mx=%d my=%d, b=%d\n",
	       xev->xbutton.x, xev->xbutton.y, xev->xbutton.button);

    /* permit the GTK event if allowed and neither Shift is pressed;
     * otherwise, tell libxine (unless we're sending GTK keyrelease events
     * for gtv button)
     */
    if (!(xev->xbutton.state & ShiftMask)
	&& (priv->button_press_mask & (1 << xev->xbutton.button)))
      return GDK_FILTER_CONTINUE;

    if (!(priv->button_release_mask & (1 << xev->xbutton.button)))
    {
      gtv_send_xine_mouse_event (priv, xev->xbutton.x, xev->xbutton.y,
				 xev->xbutton.button);
      /* record if either Shift is pressed for gtv button
       * (so that we know *not* to send the release event to the front end)
       */
      if (xev->xbutton.state & ShiftMask)
	priv->button_event_shifted |= 1 << xev->xbutton.button;
    }
    return GDK_FILTER_REMOVE;

  case ButtonRelease:
    /* permit the GTK event if allowed *and* Shift wasn't pressed when
     * the corresponding press event was received
     */
    logprintf("gtkvideo: mouse button release: mx=%d my=%d, b=%d\n",
	      xev->xbutton.x, xev->xbutton.y, xev->xbutton.button);

    if (priv->button_event_shifted & (1 << xev->xbutton.button))
      priv->button_event_shifted &= ~(1 << xev->xbutton.button);
    else if (priv->button_release_mask & (1 << xev->xbutton.button))
      return GDK_FILTER_CONTINUE;

    return GDK_FILTER_REMOVE;

#if defined HAVE_XTESTEXTENSION || defined LOG
  case KeyPress:
    {
      static char    buffer [20];
      KeySym         keysym;
      XComposeStatus compose;

      XLookupString (&xev->xkey, buffer, sizeof (buffer), &keysym, &compose);
      logprintf ("gtkvideo: key press %ld\n", (long) keysym);
#ifdef HAVE_XTESTEXTENSION
      switch (keysym)
      {
      case XK_Shift_L: shift |= 1; break;
      case XK_Shift_R: shift |= 2; break;
      }
#endif
    }
    return GDK_FILTER_CONTINUE;

  case KeyRelease:
    {
      static char    buffer [20];
      KeySym         keysym;
      XComposeStatus compose;

      XLookupString (&xev->xkey, buffer, sizeof (buffer), &keysym, &compose);
      logprintf ("gtkvideo: key release %ld\n", (long) keysym);
#ifdef HAVE_XTESTEXTENSION
      switch (keysym)
      {
      case XK_Shift_L: shift &= ~1; break;
      case XK_Shift_R: shift &= ~2; break;
      }
#endif
    }
    return GDK_FILTER_CONTINUE;
#endif /* XTEST && LOG */

  default:
    if (xev->type < LASTEvent)
      logprintf ("gtkvideo: received event %d\n", xev->type);
  }

  return GDK_FILTER_CONTINUE;
}

static gboolean configure_cb (GtkWidget *widget, GdkEventConfigure *event,
			      GtkVideo *this)
{
  this->priv->pos.width = event->x + this->widget.allocation.width;
  this->priv->pos.height = event->y + this->widget.allocation.height;
  return FALSE;
}

static void show_cb (GtkWidget *w, gpointer d)
{
  gtk_widget_set_size_request (w, w->allocation.width, w->allocation.height);
  gtk_video_allow_shrink (GTK_VIDEO (w));
}

static gboolean gtv_screen_blanker_cb (GtkVideo *gtv)
{
  gtv_unblank_screen (gtv);
  return TRUE;
}

#define MWM_HINTS_DECORATIONS   (1L << 1)
#define PROP_MWM_HINTS_ELEMENTS 5
typedef struct {
  uint32_t                    flags;
  uint32_t                    functions;
  uint32_t                    decorations;
  int32_t                     input_mode;
  uint32_t                    status;
} MWMHints;

/*
#define gtv_atom_clear(D,W,A) \
  gtv_atom_op ((D), (W), PropModeReplace, (A), NULL)
#define gtv_atom_set(D,W,A,...) \
  gtv_atom_op ((D), (W), PropModeReplace, (A), ##__VA_ARGS__, NULL)
#define gtv_atom_append(D,W,A,...) \
  gtv_atom_op ((D), (W), PropModeAppend, (A), ##__VA_ARGS__, NULL)

static void gtv_atom_op (Display *display, Window window, int mode,
			 const char *atom, ...)
{
  Atom prop = XInternAtom (display, atom, False);
  size_t size = 0, alloc = 8;
  Atom *propvalues = malloc (alloc * sizeof (Atom));
  va_list ap;
  const char *arg;

  va_start (ap, atom);
  while ((arg = va_arg (ap, const char *)))
  {
    if (size + 1 >= alloc)
      propvalues = realloc (propvalues, (alloc += 8) * sizeof (Atom));
    propvalues[size++] = XInternAtom (display, arg, False);
  }
  propvalues[size] = 0;

  XLockDisplay (display);
  XChangeProperty (display, window, prop, XA_ATOM, 32, mode,
		   (unsigned char *) &propvalues, size);
  XUnlockDisplay (display);
  free (propvalues);
}

#define gtv_layer_set(D,W,L) \
  gtv_atom_cardinal ((D), (W), "_WIN_LAYER", (L))

static void gtv_atom_cardinal (Display *display, Window window,
			       const char *atom, long value)
{
  XLockDisplay (display);
  XChangeProperty (display, window, XInternAtom (display, atom, False),
		   XA_CARDINAL, 32, PropModeReplace,
		   (unsigned char *) &value, 1);
  XUnlockDisplay (display);
}
*/

const GtkRequisition *gtk_video_get_fullscreen_geometry (GtkVideo *gtv)
{
  GdkScreen *screen = gtv_get_screen (gtv);
  Display *display = gtv_get_xdisplay (gtv);

#ifdef HAVE_XINERAMA
  if (XineramaIsActive (display))
  {
    int monitor = gdk_screen_get_monitor_at_window (screen, gtv->widget.window);
    int count;
    XineramaScreenInfo *xinerama = XineramaQueryScreens (display, &count);
    gtv->priv->fullscreen.width = xinerama[monitor].width;
    gtv->priv->fullscreen.height = xinerama[monitor].height;
    XFree (xinerama);
  }
  else
#endif
  {
    GdkRectangle geom;
    gdk_screen_get_monitor_geometry (screen, gdk_screen_get_monitor_at_window
					       (screen, gtv->widget.window),
				     &geom);
    gtv->priv->fullscreen.width = geom.width;
    gtv->priv->fullscreen.height = geom.height;
  }
  return &gtv->priv->fullscreen;
}

static void fs_resize_cb (GdkScreen *screen, GtkVideo *gtv)
{
  gtk_video_get_fullscreen_geometry (gtv);
  logprintf ("gtkvideo: (GDK) screen resized to %dx%d\n",
	     gdk_screen_get_width (screen), gdk_screen_get_height (screen));
  if (is_fullscreen (gtv->priv))
    gtk_window_resize ((GtkWindow *)gtk_widget_get_toplevel (&gtv->widget),
		       gtv->priv->fullscreen.width,
		       gtv->priv->fullscreen.height);
}

static void fs_reattach_screen_cb (GtkVideo *gtv, GdkScreen *screen,
				   gpointer data)
{
  /* CHECKME: should connect to the new screen */
  g_signal_handler_disconnect (gtv_get_screen (gtv), gtv->priv->screen_signal);
  gtv->priv->screen_signal = g_signal_connect (screen, "size-changed",
					       G_CALLBACK (fs_resize_cb), gtv);
}

static void gtk_video_realize (GtkWidget *widget)
{
  /* GdkWindowAttr attributes; */
  /* gint          attributes_mask; */
  GtkVideo            *this;
  Pixmap               bm_no;
  XColor               black_pixel;
  gtk_video_private_t *priv;
  GdkDisplay	      *display;
  GdkScreen	      *screen;
  Display	      *xdisplay;

  static char bm_no_data[] = { 0,0,0,0, 0,0,0,0 };

  g_return_if_fail(widget != NULL);
  g_return_if_fail(GTK_IS_VIDEO(widget));

  this = (GtkVideo *) widget;
  priv = this->priv;

  /* set realized flag */
  GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED);

  display = gdk_display_open (gdk_display_get_name (gdk_display_get_default ()));
  if (!display)
  {
    g_printerr (_("gtkvideo: XOpenDisplay failed!\n"));
    return;
  }

  xdisplay = GDK_DISPLAY_XDISPLAY (display);
  XLockDisplay (xdisplay);

  screen = gdk_display_get_default_screen (display);

  /*
   * create our own video window
   */

  black_pixel.pixel = BlackPixel (xdisplay, GDK_SCREEN_XNUMBER (screen));

  priv->video_window = XCreateSimpleWindow (xdisplay,
					    GDK_WINDOW_XWINDOW (gtk_widget_get_parent_window(widget)),
					    widget->allocation.x,
					    widget->allocation.y,
					    widget->allocation.width,
					    widget->allocation.height, 0,
					    black_pixel.pixel, black_pixel.pixel);

  widget->window = gdk_window_foreign_new_for_display
		     (display, priv->video_window);
  gdk_window_set_user_data (widget->window, widget); /* allow GTK events */
  {
    /* enforce background colour */
    static const GdkColor black = {};
    for (int i = 4; i >= 0; --i)
      gtk_widget_modify_bg (widget, i, &black);
  }

  /*
   * prepare for fullscreen playback
   */
  gtk_video_get_fullscreen_geometry (this);
  priv->fullscreen_mode = 0;
  priv->screen_signal = g_signal_connect (screen, "size-changed",
					  G_CALLBACK (fs_resize_cb), this);

  /*
   * track configure events of toplevel window
   */
  g_signal_connect (gtk_widget_get_toplevel (widget), "configure-event",
		    G_CALLBACK (configure_cb), this);

  /*
   * handle show events etc.
   */
  g_object_connect (G_OBJECT (widget),
	"signal::show", G_CALLBACK (show_cb), NULL,
	"signal::screen-changed", G_CALLBACK (fs_reattach_screen_cb), NULL,
	NULL);

#ifdef HAVE_XTESTEXTENSION
  {
    int dummy1 = 0, dummy2 = 0, dummy3 = 0, dummy4 = 0;

    priv->have_xtest = XTestQueryExtension (xdisplay, &dummy1, &dummy2, &dummy3, &dummy4);
    priv->kc_shift[0] = XKeysymToKeycode (xdisplay, XK_Shift_L);
    priv->kc_shift[1] = XKeysymToKeycode (xdisplay, XK_Shift_R);
  }
#endif

  gdk_window_set_events (widget->window, GDK_ALL_EVENTS_MASK);
  XSelectInput (xdisplay, priv->video_window,
		StructureNotifyMask | ExposureMask | FocusChangeMask |
		KeyPressMask | KeyReleaseMask | PointerMotionMask |
		ButtonPressMask | ButtonReleaseMask | ButtonMotionMask);
  gdk_window_add_filter (widget->window, (GdkFilterFunc) xevent_filter_cb,
			 widget);

  XUnlockDisplay (xdisplay);

  /*
   * load audio, video drivers
   */

  priv->video_port = load_video_out_driver (this);

  if (!priv->video_port)
  {
    g_printerr (_("gtkvideo: couldn't open video driver\n"));
    return;
  }

  /*
   * create mouse pointers
   */

  XLockDisplay (xdisplay);
  bm_no = XCreateBitmapFromData (xdisplay, priv->video_window,
				 bm_no_data, 8, 8);
  priv->pointer[0] = XCreatePixmapCursor (xdisplay, bm_no, bm_no,
					  &black_pixel, &black_pixel, 0, 0);
  priv->pointer[1] = XCreateFontCursor (xdisplay, XC_left_ptr);
  priv->pointer_visible = TRUE;
  XUnlockDisplay (xdisplay);

  priv->screen_blanker_timeout =
    g_timeout_add (4000, (GSourceFunc) gtv_screen_blanker_cb, gtv);
}


xine_video_port_t *gtk_video_get_port (GtkVideo *gtv)
{
  gtk_video_private_t *priv = gtv->priv;
  return priv->video_port;
}

static void gtk_video_unrealize (GtkWidget *widget)
{
  GtkVideo            *this;
  gtk_video_private_t *priv;

  g_return_if_fail(widget != NULL);
  g_return_if_fail(GTK_IS_VIDEO(widget));

  this = (GtkVideo *) widget;
  priv = this->priv;

  if (priv->screen_blanker_timeout)
  {
    g_source_remove (priv->screen_blanker_timeout);
    priv->screen_blanker_timeout = 0;
  }

  /* stop event thread */
  if (priv->thread)
    pthread_cancel (priv->thread);

  /* Hide all windows */
  if (GTK_WIDGET_MAPPED (widget))
    gtk_widget_unmap (widget);

  GTK_WIDGET_UNSET_FLAGS (widget, GTK_MAPPED);

  /* This destroys widget->window and unsets the realized flag */
  if (GTK_WIDGET_CLASS(parent_class)->unrealize)
    (* GTK_WIDGET_CLASS(parent_class)->unrealize) (widget);
}

static void gtk_video_map (GtkWidget *widget)
{
  GdkGeometry geom;
  parent_class->map (widget);
  if (!((GtkVideo *)widget)->priv->hinted)
  {
    ((GtkVideo *)widget)->priv->hinted = TRUE;
    geom.min_width = geom.min_height = -1;
    gtk_window_set_geometry_hints (GTK_WINDOW (gtk_widget_get_toplevel (widget)),
				   widget, &geom, GDK_HINT_MIN_SIZE);
  }
}

GtkWidget *gtk_video_new (xine_t *xine, xine_stream_t *stream,
    		          xine_post_out_t *out,
                          const gchar *video_driver_id,
                          int default_width, int default_height,
                          int button_press_mask, int button_release_mask)
{
  GtkWidget	      *this = g_object_new (gtk_video_get_type (), NULL);
  GtkVideo            *gtv = (GtkVideo *) this;
  gtk_video_private_t *priv;

  gtv->priv = priv = g_malloc0 (sizeof (gtk_video_private_t));

  priv->xine = xine;
  priv->stream = stream;
  priv->out = out;
  priv->resize_factor = 50;
  priv->auto_resize = TRUE;
  priv->size = (GtkRequisition){ default_width, default_height };
  priv->video_size = priv->size;
  priv->button_press_mask = button_press_mask;
  priv->button_release_mask = button_release_mask;
  priv->vis_plugin_id = NULL;
  priv->vis_plugin = NULL;
  priv->post_deinterlace.list = NULL;
  priv->post_video.list = NULL;
  priv->post_audio.list = NULL;

  if (video_driver_id)
    priv->video_driver_id = strdup (video_driver_id);
  else
    priv->video_driver_id = NULL;

  gtv_init_dbus (gtv);

  return this;
}

static void gtk_video_size_allocate (GtkWidget *widget,
    				     GtkAllocation *allocation)
{
  GtkVideo *this;

  g_return_if_fail (widget != NULL);
  g_return_if_fail(GTK_IS_VIDEO(widget));

  this = (GtkVideo *) widget;

  if (GTK_WIDGET_REALIZED (widget))
  {
    gboolean send_signal = FALSE;
    widget->allocation = *allocation;
    if (is_fullscreen (this->priv))
    {
      widget->allocation.width = this->priv->fullscreen.width;
      widget->allocation.height = this->priv->fullscreen.height;
    }
    gdk_window_move_resize (widget->window, allocation->x, allocation->y,
			    widget->allocation.width,
			    widget->allocation.height);
    if (this->priv->old_video_size.width && !this->priv->resizing
	&& !this->priv->idle_resized
	&& !is_fullscreen (this->priv)
	&& GTK_WIDGET_VISIBLE (widget)
	&& widget->allocation.width > GTK_VIDEO_MIN_WIDTH
	&& widget->allocation.height > GTK_VIDEO_MIN_HEIGHT)
    {
      gtk_video_private_t *const priv = this->priv;
      double new_factor;
      if (priv->pending_resize)
	new_factor = priv->resize_factor;
      else
      {
	gboolean is_double =
	  IS_DOUBLE(priv->old_video_size.width, priv->old_video_size.height);
	new_factor = this->widget.allocation.width * 100
		     / (double)priv->old_video_size.width / (1 + is_double);
	double new_factor_V = this->widget.allocation.height * 100
			      / (double)priv->old_video_size.height
			      / (1 + is_double);
	if (new_factor > new_factor_V)
	  new_factor= new_factor_V;
      }
      /* tolerate rounding error when the video size has been changed... */
      if (priv->pending_resize
	  || (round (priv->resize_factor * 64) != round (new_factor * 64)
	      && (fabs (new_factor - priv->resize_factor)
		    >= 25 / this->widget.allocation.height
		  || priv->old_video_size.height == priv->video_size.height)))
      {
	if (!priv->block_next_scale_change && GTK_WIDGET_VISIBLE (widget))
	{
	  priv->resize_factor = new_factor;
	  send_signal = TRUE;
	  /* stop frame_output_cb from scheduling a resize */
	  double factor = DOUBLE_STD();
	  priv->size = SCALE (factor);
	  logprintf ("gxine: video rescaled to %lf%%\n", new_factor);
	}
        else
	  logprintf ("blocked rescaling to %lf%%\n", new_factor);
      }
    }
    if (!this->priv->resizing)
      this->priv->old_video_size = this->priv->video_size;
    this->priv->idle_resized = FALSE;

    if (send_signal)
      g_signal_emit ((GObject *)this,
		     gtv_table_signals[GTK_VIDEO_SCALE_FACTOR], 0,
		     this->priv->resize_factor);
  }
  else
    widget->allocation = *allocation;

  if (widget->window)
  {
    /* FIXME: stop something unknown (as of 20051014) from blanking the window
     * (no map/unmap event handlers are expected)
     */
    gtk_widget_unmap (widget);
    gtk_widget_map (widget);
  }
}

void gtk_video_resize (GtkVideo *gtv,
		       gint x, gint y,
		       gint width, gint height)
{
  GtkAllocation allocation = { x, y, width, height };
  gtk_video_size_allocate (&gtv->widget, &allocation);
}

void gtk_video_rescale (GtkVideo *gtv, double scale)
{
  RESIZE_LOCK ();
  {
    gtk_video_private_t *priv = gtv->priv;
    if (scale == -1)
      scale = priv->resize_factor;
    double factor = DOUBLE (scale, priv->video_size.width,
				  priv->video_size.height);
    priv->size = SCALE (factor);
    priv->resize_factor = scale;
    logprintf ("rescale: size = %d,%d factor=%f\n",
	       priv->size.width, priv->size.height, scale);
    if (is_fullscreen (priv) || !GTK_WIDGET_VISIBLE (&gtv->widget))
      priv->pending_resize = TRUE;
    else
      gtv_do_rescale (gtv);
  }
  RESIZE_UNLOCK ();
}

static gboolean unblock_cb (gpointer gtv)
{
  logprintf ("unblocking rescale\n");
  ((GtkVideo *)gtv)->priv->block_next_scale_change = FALSE;
  ((GtkVideo *)gtv)->priv->unblock_timeout = 0;
  return FALSE;
}

static void gtv_unblock_timeout (GtkVideo *gtv)
{
  if (gtv->priv->unblock_timeout)
    g_source_remove (gtv->priv->unblock_timeout);
  gtv->priv->block_next_scale_change = TRUE;
  gtv->priv->unblock_timeout = g_timeout_add (250, unblock_cb, gtv);
}

void gtk_video_reshow (GtkVideo *gtv)
{
  if (gtv && GTK_IS_VIDEO (gtv))
  {
    gtv->priv->block_next_scale_change = TRUE;
    gtk_widget_show (&gtv->widget);
    gtk_widget_set_size_request (&gtv->widget,
				 gtv->priv->size.width, gtv->priv->size.height);
    gdk_flush ();
    gtv_unblock_timeout (gtv);
    gtk_video_allow_shrink (gtv);
  }
}

void gtk_video_set_fullscreen (GtkVideo *gtv, gint fullscreen)
{
  gtk_video_private_t  *priv;
  GtkWindow	       *toplevel;
  Display	       *display;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  if (fullscreen == is_fullscreen (priv))
    return;

  g_static_rec_mutex_lock (&engine_lock);
  display = gtv_get_xdisplay (gtv);
  XLockDisplay (display);
  toplevel = GTK_WINDOW (gtk_widget_get_toplevel (&gtv->widget));

  if (fullscreen)
  {
    priv->fullscreen_mode = fullscreen;

    do_pending_events (); /* there may be pending resize events... */
    priv->old_size.width = gtv->widget.allocation.width;
    priv->old_size.height = gtv->widget.allocation.height;
    /* just fullscreen it - beyond that, rely on window-state-event */
    gtk_window_fullscreen (toplevel);
    do_pending_events (); /* events have been generated - process them */
    gtk_video_get_fullscreen_geometry (gtv);
    gtv_hide_pointer (gtv);
    gtv_screensaver_maybe_inhibit_dbus (gtv->priv, TRUE);
  }
  else
  {
    priv->fullscreen_mode = 2; /* special state :-) */

    priv->block_next_scale_change = TRUE;
    gtk_widget_set_size_request (&gtv->widget,
				 priv->size.width, priv->size.height);
    gdk_flush ();
    /* just unfullscreen it - beyond that, rely on window-state-event */
    gtk_window_unfullscreen (toplevel);
    gtv_screensaver_maybe_inhibit_dbus (gtv->priv, FALSE);
    gtv_show_pointer (gtv);
    gdk_flush ();
    sched_yield (); /* hmm, seems to be needed here... */

    /* resize the window, one way or another */
    if (priv->pending_resize)
      gtv_do_rescale (gtv);

    do_pending_events (); /* events have been generated - process them */
    gtv_unblock_timeout (gtv); /* idle may be called early :-\ */
    gtk_video_allow_shrink (gtv);

    priv->fullscreen_mode = FALSE;
  }

  XUnlockDisplay (display);
  g_static_rec_mutex_unlock (&engine_lock);
}

gint gtk_video_is_fullscreen (GtkVideo *gtv)
{
  g_return_val_if_fail (gtv != NULL, 0);
  g_return_val_if_fail (GTK_IS_VIDEO (gtv), 0);

  return !!gtv->priv->fullscreen_mode;
}

void gtk_video_set_auto_resize (GtkVideo *gtv, gboolean resize)
{
  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));

  gtv->priv->auto_resize = resize;
}

gboolean gtk_video_get_auto_resize (GtkVideo *gtv)
{
  g_return_val_if_fail (gtv != NULL, 0);
  g_return_val_if_fail (GTK_IS_VIDEO (gtv), 0);

  return gtv->priv->auto_resize;
}

void gtk_video_set_windowed_unblank (GtkVideo *gtv, gboolean unblank)
{
  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));

  gtv->priv->playing_no_blank = unblank;
}

gboolean gtk_video_get_windowed_unblank (GtkVideo *gtv)
{
  g_return_val_if_fail (gtv != NULL, 0);
  g_return_val_if_fail (GTK_IS_VIDEO (gtv), 0);

  return gtv->priv->playing_no_blank;
}

void
gtk_video_deny_shrink (GtkVideo *gtv)
{
  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));

  if (gtv->priv->minimising)
  {
    g_source_remove (gtv->priv->minimising);
    gtv->priv->minimising = 0;
  }
  gtv->widget.requisition = gtv->priv->pending_resize
			    ? gtv->priv->size : gtv->priv->old_size;
  g_object_set (gtv,
		"width-request", gtv->widget.requisition.width,
		"height-request", gtv->widget.requisition.height,
		NULL);
}

static gboolean
gtv_allow_shrink (gpointer gtv)
{
  if (is_fullscreen (((GtkVideo *)gtv)->priv))
    return FALSE;

  ((GtkVideo *)gtv)->priv->minimising = 0;
  ((GtkVideo *)gtv)->widget.requisition =
    (GtkRequisition){ GTK_VIDEO_MIN_WIDTH, GTK_VIDEO_MIN_HEIGHT };
  g_object_set (gtv, "width-request", -1, "height-request", -1, NULL);
  return FALSE;
}

gboolean
gtk_video_allow_shrink (GtkVideo *gtv)
{
  if (gtv)
  {
    if (gtv->priv->minimising)
      g_source_remove (gtv->priv->minimising);
    gtv->priv->minimising = g_timeout_add (1000, gtv_allow_shrink, gtv);
  }
  return FALSE;
}

/* Plugins: shared post-plugin code */

static GList *gtv_create_plugins (gtk_video_private_t *priv,
				  xine_audio_port_t **audio,
				  xine_video_port_t **video,
				  const char *plugins, int type,
				  gboolean is_deinterlace)
{
  GList *list = NULL;

  if (is_deinterlace)
    priv->have_tvtime = FALSE;

  if (!plugins || !plugins[0])
    return NULL;

  for (;;)
  {
    char *next = strchr (plugins, ';') ? : (char *)plugins + strlen (plugins);
    char *args = strchr (plugins, ':');

    if (*plugins != '-')
    {
      char *name;
      xine_post_t *plugin;

      if (args > next)
	args = NULL;

      logprintf ("plugin: %p %.*s\n", plugins, (int)((args?:next) - plugins), plugins);
      if (args)
	logprintf ("args  : %p %.*s\n", args ? args + 1 : NULL,
		   (int)(args ? next - args - 1 : 0), args ? args + 1 : NULL);
      logprintf ("next  : %p\n", next);

      name = g_strndup (plugins, (args ? : next) - plugins);
      if (is_deinterlace && !g_strcasecmp (name, "tvtime"))
	priv->have_tvtime = TRUE;
      plugin = xine_post_init (priv->xine, name, 0, audio, video);
      if (plugin)
      {
	if (plugin->type == type)
	{
	  list = g_list_append (list, plugin);
	  if (args)
	  {
	    args = g_strndup (args + 1, next - args - 1);
	    post_parse_set_parameters (plugin, args);
	    free (args);
	  }
	}
	else
	{
	  g_printerr (("gtkvideo: post-plugin '%s': wrong type (wanted %d, got %d)\n"), name, type, plugin->type);
	  xine_post_dispose (priv->xine, plugin);
	}
      }
      else
	g_printerr (_("gtkvideo: unknown post-plugin ‘%s’\n"), name);
      free (name);
    }

    if (!*next)
      break;
    plugins = next + 1;
  }

  return list;
}

static void gtv_dispose_plugin (xine_post_t *post, xine_t *xine)
{
  xine_post_dispose (xine, post);
}

static void gtv_dispose_plugins (gtk_video_private_t *priv, GList *list)
{
  g_list_foreach (list, (GFunc) gtv_dispose_plugin, priv->xine);
  g_list_free (list);
}

static xine_post_out_t *gtv_post_output (xine_post_t *post,
					 const char *const s[])
{
  return xine_post_output (post, s[0])
	 ? : xine_post_output (post, s[1])
	     ? : xine_post_output (post, xine_post_list_outputs (post)[0]);
}

static xine_post_in_t *gtv_post_input (xine_post_t *post,
				       const char *const s[])
{
  return xine_post_input (post, s[0])
	 ? : xine_post_input (post, s[1])
	     ? : xine_post_input (post, xine_post_list_inputs (post)[0]);
}

/* Plugins: video post-plugins */

static const char *const video_io[] = { "video out", "video", "video in" };

static xine_post_out_t *gtv_post_output_video (xine_post_t *post)
{
  return gtv_post_output (post, video_io);
}

static xine_post_in_t *gtv_post_input_video (xine_post_t *post)
{
  return gtv_post_input (post, video_io + 1);
}

static void gtv_wire_plugins_video (gtk_video_private_t *priv)
{
  /* note: assumes default wiring */
  GList *list = priv->post_video.enable
		? g_list_copy (priv->post_video.list) : NULL;
  GList *item, *next = NULL;

  if (priv->post_deinterlace.list && priv->post_deinterlace.enable)
    list = g_list_concat (g_list_copy (priv->post_deinterlace.list), list);

  xine_post_wire_video_port (xine_get_video_source (priv->stream),
			     priv->video_port);

  item = g_list_last (list);
  while (item)
  {
    xine_post_out_t *out = gtv_post_output_video (item->data);
    if (next)
      xine_post_wire (out, gtv_post_input_video (next->data));
    else
      xine_post_wire_video_port (out, priv->video_port);
    next = item;
    item = g_list_previous (item);
  }

  if (next)
    xine_post_wire (xine_get_video_source (priv->stream),
		    gtv_post_input_video (next->data));

  g_list_free (list);

  xine_set_param (priv->stream, XINE_PARAM_VO_DEINTERLACE,
		  priv->post_deinterlace.enable &&
		    !priv->post_deinterlace.list);
}

static void gtv_unwire_plugins_video (gtk_video_private_t *priv)
{
  GList *item;
  xine_post_wire_video_port (xine_get_video_source (priv->stream),
			     priv->video_port);
  item = g_list_last (priv->post_video.list);
  while (item)
  {
    xine_post_wire (NULL, gtv_post_input_video (item->data));
    item = g_list_previous (item);
  }
}

void gtk_video_set_post_plugins_video (GtkVideo *gtv, const char *params)
{
  gtk_video_private_t *priv;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  gtv_unwire_plugins_video (priv);
  gtv_dispose_plugins (priv, priv->post_video.list);
  priv->post_video.list =
    gtv_create_plugins (priv, NULL, &priv->video_port, params,
			XINE_POST_TYPE_VIDEO_FILTER, FALSE);
  gtv_wire_plugins_video (priv);
}

void gtk_video_set_use_post_plugins_video (GtkVideo *gtv, gboolean on)
{
  gtk_video_private_t *priv;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  gtv_unwire_plugins_video (priv);
  priv->post_video.enable = on;
  gtv_wire_plugins_video (priv);
}

gboolean gtk_video_get_use_post_plugins_video (GtkVideo *gtv)
{
  return (gtv && gtv->priv) ? gtv->priv->post_video.enable : FALSE;
}


/* Plugins: audio post-plugins */

static const char *const audio_io[] = { "audio out", "audio", "audio in" };

static xine_post_out_t *gtv_post_output_audio (xine_post_t *post)
{
  return gtv_post_output (post, audio_io);
}

static xine_post_in_t *gtv_post_input_audio (xine_post_t *post)
{
  return gtv_post_input (post, audio_io + 1);
}

static void gtv_wire_plugins_audio (gtk_video_private_t *priv,
				    xine_audio_port_t *audio_port)
{
  /* note: assumes default wiring */
  GList *list = priv->post_audio.enable
		? g_list_copy (priv->post_audio.list) : NULL;
  GList *item, *next = NULL;

  if (priv->vis_active && priv->vis_plugin)
    list = g_list_prepend (list, priv->vis_plugin);

  item = g_list_last (list);
  while (item)
  {
    xine_post_out_t *out = gtv_post_output_audio (item->data);
    if (next)
      xine_post_wire (out, gtv_post_input_audio (next->data));
    else
      xine_post_wire_audio_port (out, audio_port);
    next = item;
    item = g_list_previous (item);
  }

  if (next)
    xine_post_wire (xine_get_audio_source (priv->stream),
		    gtv_post_input_audio (next->data));

  g_list_free (list);
}

static void gtv_unwire_plugins_audio (gtk_video_private_t *priv,
				      xine_audio_port_t *audio_port)
{
  GList *item;
  xine_post_wire_audio_port (xine_get_audio_source (priv->stream), audio_port);
  item = g_list_last (priv->post_audio.list);
  while (item)
  {
    xine_post_wire (NULL, gtv_post_input_audio (item->data));
    item = g_list_previous (item);
  }
  if (priv->vis_plugin)
    xine_post_wire (NULL, gtv_post_input_audio (priv->vis_plugin));
}

void gtk_video_set_post_plugins_audio (GtkVideo *gtv, const char *params,
				       xine_audio_port_t *audio_port)
{
  gtk_video_private_t *priv;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  gtv_unwire_plugins_audio (priv, audio_port);
  gtv_dispose_plugins (priv, priv->post_audio.list);
  priv->post_audio.list =
    gtv_create_plugins (priv, &audio_port, NULL, params,
			XINE_POST_TYPE_AUDIO_FILTER, FALSE);
  gtv_wire_plugins_audio (priv, audio_port);
}

void gtk_video_set_use_post_plugins_audio (GtkVideo *gtv, gboolean on,
					   xine_audio_port_t *audio_port)
{
  gtk_video_private_t *priv;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  priv->post_audio.enable = on;
  gtv_unwire_plugins_audio (priv, audio_port);
  gtv_wire_plugins_audio (priv, audio_port);
}

gboolean gtk_video_get_use_post_plugins_audio (GtkVideo *gtv)
{
  return (gtv && gtv->priv) ? gtv->priv->post_audio.enable : FALSE;
}


/* Plugins: audio visualisation post-plugins */

static void
gtv_wire_plugin_vis (gtk_video_private_t *priv, xine_audio_port_t *audio_port)
{
  if (!priv->vis_plugin)
    return;

  if (priv->post_audio.list)
    xine_post_wire (gtv_post_output_audio (priv->vis_plugin),
		    gtv_post_input_audio (priv->post_audio.list->data));
  else
    xine_post_wire_audio_port (gtv_post_output_audio (priv->vis_plugin),
			       audio_port);
  xine_post_wire (xine_get_audio_source (priv->stream),
		  gtv_post_input_audio (priv->vis_plugin));
}

static void
gtv_unwire_plugin_vis (gtk_video_private_t *priv, xine_audio_port_t *audio_port)
{
  if (!priv->vis_plugin)
    return;

  if (priv->post_audio.list)
    xine_post_wire (xine_get_audio_source (priv->stream),
		    gtv_post_input_audio (priv->post_audio.list->data));
  else
    xine_post_wire_audio_port (xine_get_audio_source (priv->stream),
			       audio_port);
  xine_post_wire (NULL, gtv_post_input_audio (priv->vis_plugin));
}

static void gtv_recreate_vis (gtk_video_private_t *priv,
			      xine_audio_port_t **audio_port)
{
  if (priv->vis_plugin)
    xine_post_dispose (priv->xine, priv->vis_plugin);
  if (priv->vis_active && priv->vis_plugin_id)
    priv->vis_plugin = xine_post_init (priv->xine, priv->vis_plugin_id, 0,
				       audio_port, &priv->video_port);
  else
    priv->vis_plugin = NULL;
}

gboolean gtk_video_select_vis (GtkVideo *gtv, const char *id,
			       xine_audio_port_t **audio_port)
{
  gtk_video_private_t *priv;

  g_return_val_if_fail (gtv != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_VIDEO (gtv), FALSE);
  priv = gtv->priv;
  g_return_val_if_fail (priv->stream != NULL, FALSE);

  if (!priv->vis_plugin_id && (!id || !strcasecmp (id, "none")))
    return FALSE; /* same lack of plugin => nothing to do */

  if (priv->vis_plugin_id && id && !strcasecmp (priv->vis_plugin_id, id))
    return priv->vis_plugin != NULL; /* same plugin => nothing to do */

  if (priv->vis_active)
    gtv_unwire_plugin_vis (priv, *audio_port);
  free (priv->vis_plugin_id);
  priv->vis_plugin_id = id ? strdup (id) : NULL;
  gtv_recreate_vis (priv, audio_port);
  if (priv->vis_active)
    gtv_wire_plugin_vis (priv, *audio_port);

  return priv->vis_plugin != NULL;
}

gboolean gtk_video_set_vis (GtkVideo *gtv, xine_audio_port_t **audio_port,
			    gboolean active)
{
  gtk_video_private_t  *priv;

  g_return_val_if_fail (gtv != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_VIDEO (gtv), FALSE);
  priv = gtv->priv;
  g_return_val_if_fail (priv->stream != NULL, FALSE);

  if (priv->vis_active == active)
    return priv->vis_plugin != NULL; /* same state => nothing to do */

  if (priv->vis_active)
    gtv_unwire_plugin_vis (priv, *audio_port);
  priv->vis_active = active;
  gtv_recreate_vis (priv, audio_port);
  if (priv->vis_active)
    gtv_wire_plugin_vis (priv, *audio_port);

  return priv->vis_plugin != NULL;
}


/* Plugins: deinterlace post-plugin */

void gtk_video_set_post_plugins_deinterlace (GtkVideo *gtv, const char *params)
{
  gtk_video_private_t *priv;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  gtv_unwire_plugins_video (priv);
  gtv_dispose_plugins (priv, priv->post_deinterlace.list);
  priv->post_deinterlace.list =
    gtv_create_plugins (priv, NULL, &priv->video_port, params,
			XINE_POST_TYPE_VIDEO_FILTER, TRUE);
  gtv_wire_plugins_video (priv);

  /* allow the old method if there are no created deinterlace plugins */
  xine_set_param (priv->stream, XINE_PARAM_VO_DEINTERLACE,
		  priv->post_deinterlace.enable &&
		    (!priv->post_deinterlace.list || priv->have_tvtime));
}

void gtk_video_set_use_post_plugins_deinterlace (GtkVideo *gtv, gboolean on)
{
  gtk_video_private_t *priv;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  gtv_unwire_plugins_video (priv);
  priv->post_deinterlace.enable = on;
  gtv_wire_plugins_video (priv);

  /* allow the old method if there are no created deinterlace plugins */
  xine_set_param (priv->stream, XINE_PARAM_VO_DEINTERLACE,
		  on && (!priv->post_deinterlace.list || priv->have_tvtime));
}

gboolean gtk_video_get_use_post_plugins_deinterlace (GtkVideo *gtv)
{
  return (gtv && gtv->priv) ? gtv->priv->post_deinterlace.enable : FALSE;
}


/* Other bits */

void gtk_video_set_auto_rescale (GtkVideo *gtv, gboolean set)
{
  gtk_video_private_t *priv;

  g_return_if_fail (gtv != NULL);
  g_return_if_fail (GTK_IS_VIDEO (gtv));
  priv = gtv->priv;
  g_return_if_fail (priv->stream != NULL);

  priv->auto_rescale = set;

  RESIZE_ADD ();
}

gboolean gtk_video_get_auto_rescale (GtkVideo *gtv)
{
  return (gtv && gtv->priv) ? gtv->priv->auto_rescale : FALSE;
}

static int screensaver_poke (const char *const argv[], const char *name)
{
  int ret = 60; /* default return = try again in 4 minutes */
  GError *err = NULL;

  g_spawn_async (NULL, (gchar **)argv, NULL,
		 G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL,
		 NULL, NULL, NULL, &err);
  if (err)
  {
    g_printerr (_("gtkvideo: can't poke %s: %s\n"), name, err->message);
    if (err->domain == G_SPAWN_ERROR)
      switch (err->code)
      {
      case G_SPAWN_ERROR_ACCES:
      case G_SPAWN_ERROR_PERM:
      case G_SPAWN_ERROR_NOEXEC:
      case G_SPAWN_ERROR_NOENT:
      case G_SPAWN_ERROR_ISDIR:
	g_printerr (_("gtkvideo: not trying again\n"));
	ret = -1;
	break;
      default:
	ret = 60;
	break;
      }
    g_error_free (err);
  }

  return ret;
}

static gboolean gtv_unblank_screen (GtkVideo *gtv)
{
  /* (fullscreen || (deny blank && drawable && playing)) && !logo && !paused */
  if (!((is_fullscreen (gtv->priv)
	 || (gtv->priv->playing_no_blank && GTK_WIDGET_DRAWABLE (&gtv->widget)
	     && xine_get_status (gtv->priv->stream) == XINE_STATUS_PLAY))
	&& !playlist_showing_logo ()
	&& xine_get_param (gtv->priv->stream, XINE_PARAM_SPEED)))
    return FALSE;

  Display *display = gtv_get_xdisplay (gtv);
  XLockDisplay (display);
#ifdef HAVE_DPMS_EXTENSION
  union { int i; CARD16 c; } dummy;
  BOOL enabled;
  if (DPMSQueryExtension (display, &dummy.i, &dummy.i) && DPMSCapable (display)
      && DPMSInfo (display, &dummy.c, &enabled) && enabled)
    DPMSForceLevel (display, DPMSModeOn);
#endif
#ifdef HAVE_XTESTEXTENSION
  gtk_video_private_t *priv = gtv->priv;
  if (priv->have_xtest == True)
  {
    static unsigned char flip = 0;
    int state = !(shift & ((flip = !flip) + 1));
    XTestFakeKeyEvent (display, priv->kc_shift[flip], state, CurrentTime);
    XTestFakeKeyEvent (display, priv->kc_shift[flip], !state, CurrentTime);
    XSync (display, False);
  }
  else
#endif
  {
    XForceScreenSaver (display, ScreenSaverReset);
  }

  if (!screensaver_is_running_dbus (priv))
  {
#ifdef GNOME_SCREENSAVER_COMMAND
    /* poke GNOME screensaver (assume that $PATH is sane) */
    static int gnome_screensaver = 0;
    if (gnome_screensaver > 0)
      --gnome_screensaver;
    if (!gnome_screensaver)
    {
      static const char *const argv[] = { GNOME_SCREENSAVER_COMMAND, "--poke", NULL };
      /* screensaver name is used in "can't poke <name>" */
      gnome_screensaver = screensaver_poke (argv, _("gnome-screensaver"));
    }
#endif

#ifdef X_SCREENSAVER_COMMAND
    /* similarly, poke xscreensaver */
    static int x_screensaver = 0;
    if (x_screensaver > 0)
      --x_screensaver;
    if (!x_screensaver)
    {
      static const char *const argv[] = { X_SCREENSAVER_COMMAND, "--deactivate", NULL };
      x_screensaver = screensaver_poke (argv, _("xscreensaver"));
    }
#endif
  }

  XUnlockDisplay (display);

  return FALSE;
}

void gtk_video_unblank_screen (GtkVideo *gtv)
{
  g_idle_add ((GSourceFunc) gtv_unblank_screen, gtv);
}
